package com.atguigu.gmall.gateway.component;

import com.atguigu.gmall.common.result.Result;
import com.atguigu.gmall.common.result.ResultCodeEnum;
import com.atguigu.gmall.common.util.IpUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseCookie;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.io.UnsupportedEncodingException;
import java.util.Map;
import java.util.Set;

/**
 * @author dyqstart
 * @create 2021-08-16-20:52
 * @since 1.0.0
 */
@Data
@ConfigurationProperties(prefix = "authinfo")
@Component
public class AuthFilter implements GlobalFilter {

    //匹配器
    private AntPathMatcher matcher = new AntPathMatcher();

    private String needLoginPathPattern = "/api/**/auth/**";


    //    @Value("${authinfo.urls}")//简单取值
    private Set<String> urls;

    @Autowired
    StringRedisTemplate redisTemplate;


    /**
     * Webflux的编程方式（响应式编程）【0/1，  多个N 】
     * - Mono： 返回0个或者一个数据
     * - FLux： 返回多个数据
     *
     * @param exchange 包含了request和respone
     * @param chain    filter链  chain.doFilter(request,response,chain)
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

        //1、获取请求相应
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();

        //所有请求
        String path = request.getURI().getPath();
        if (matcher.match("/**/inner/**", path)) {
            //匹配拒绝访问
            return responseWriteDate(response, ResultCodeEnum.PERMISSION);
        }

        //2、判断是否登录，如果登录了。再验证合法性
        //获取到当前用户id  null 未登录|| -1登录了是错的||成功
        String userId = getUserId(request);
        if ("-1".equals(userId)) {
            //登录了但是不合法
            return responseWriteDate(response, ResultCodeEnum.LOGIN_AUTH);
        }

        /**
         * 以下需要登录的，就由网关达到登录页面
         *
         * 能走到这里，一定不是 -1 \\要么是null要么有值的
         */
        //3、系统规范 /api/**/auth/**,这些接口需要登录
        if (matcher.match(needLoginPathPattern, path) && StringUtils.isEmpty(userId)) {
            //需要登录，但是前面返回的是null,没有登录。返回错误
            return responseWriteLocation(request, response);
        }

        //4、为了增强扩展性，希望有一个配置，直接配置哪些需要登录，网关就自己校验
        if (urls.contains(path) && StringUtils.isEmpty(userId)) {
            //配置的请求需要登录，但是没有
            return responseWriteLocation(request, response);
        }


        //TODO 不清楚mutate
        ServerHttpRequest.Builder requestBuilder = request.mutate();
        //包装模式

        //如果cookie中有userTempId:以请求头的方式往下透传
        String userTempId = getUserTempId(request);
        if (!StringUtils.isEmpty(userTempId)){
            requestBuilder.header("userTempId", userTempId);
        }

        //透传userId,给request的请求头上设置一个userId
        //请求再放给其他微服务，要么null,要么真的用户id
        if (!StringUtils.isEmpty(userId)) {
            //添加自定义userId请求头，原来的请求不能直接修改
//            HttpHeaders headers = request.getHeaders();
//            headers.set("userId", userId);
            requestBuilder.header("userId", userId);
        }
        ServerHttpRequest build = requestBuilder.build();
        ServerWebExchange exchangeMutate = exchange.mutate().request(build).response(response).build();
        //过滤器继续往下
        return chain.filter(exchangeMutate);
    }


    //如果cookie中有userTempId，以请求头的方式放下透传
    private String getUserTempId(ServerHttpRequest request) {
        //如果登陆了，就自己返回用户id,同时要进行合法性校验
        String userTempId = request.getHeaders().getFirst("userTempId");
        if (StringUtils.isEmpty(userTempId)) {
            //正常请求头的token是空的，安卓、ios会响应头会携带
            HttpCookie cookie = request.getCookies().getFirst("userTempId");
            if (cookie != null) {
                userTempId = cookie.getValue();
            }
        }
        return userTempId;
    }


    /**
     * 重定向到登录页
     *
     * @param request
     * @param response
     * @return
     */
    private Mono<Void> responseWriteLocation(ServerHttpRequest request, ServerHttpResponse response) {
        //让浏览器重定向到登录页
        response.setStatusCode(HttpStatus.FOUND);
        //浏览器一看状态码是302，自己解析响应头的Location字段，浏览器自己跳过去

        //登录的是错误的，需要清除以前的登录cookie
        /**
         *  1、服务器命令浏览器-保存cookie，响应头会带 Set-Cookie： token=xxxx  默认失效，session
         *  response.addCookie(xxxx);
         *  2、删除cookie、Set-Cookie、token=xxxx,默认时效：0
         *  3、命令浏览器保存cookie的时候，如果时间>0,浏览器保存，如果=0，浏览器就销毁掉它
         */

        ResponseCookie tokenCookie = ResponseCookie
                .from("token", "xxxx")
                .maxAge(0L)
                .domain("gmall.com")
                .build();
        //删除旧的令牌
        response.addCookie(tokenCookie);

        //删除旧的登录信息 userInfo
        ResponseCookie userInfoCookie = ResponseCookie
                .from("userInfo", "xxx")
                .domain("gmall.com")
                .maxAge(0L)
                .build();
        response.addCookie(userInfoCookie);
        //浏览器会自动删除指定的cookie



        HttpHeaders headers = response.getHeaders();
        String string = request.getURI().toString();
        String url = "http://passport.gmall.com/login.html?originUrl=" + string;
        headers.set(HttpHeaders.LOCATION, url);

        return response.setComplete();
    }

    private String getUserId(ServerHttpRequest request) {
        //如果登陆了，就自己返回用户id,同时要进行合法性校验,ajax里面有
        String token = request.getHeaders().getFirst("token");
        if (StringUtils.isEmpty(token)) {
            //正常请求头的token是空的，安卓、ios会响应头会携带
            HttpCookie cookie = request.getCookies().getFirst("token");
            if (cookie != null) {
                token = cookie.getValue();
            }

        }
        //拿到了token
        if (StringUtils.isEmpty(token)) {
            return null;
        } else {
            //验证合法性：
            String key = "user:login:" + token;//就能去redis查询到用户信息
            String json = redisTemplate.opsForValue().get(key);
            if (StringUtils.isEmpty(json)) {
                return "-1";
            }
            Map<String, String> map = null;
            try {
                map = new ObjectMapper().readValue(json, new TypeReference<Map<String, String>>() {
                });
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }

            //拿到redis中的用户id
            String userId = map.get("userId");
            String ipAddr = map.get("ipAddr");
            //放冒牌
            String ipAddress = IpUtil.getGatwayIpAddress(request);
            if (ipAddress.equals(ipAddr)) {
                return userId;
            } else {
                return "-1";
            }
        }
    }

    /**
     * 给浏览器相应没有权限的json
     *
     * @param response
     * @param permission
     * @return
     */
    private Mono<Void> responseWriteDate(ServerHttpResponse response, ResultCodeEnum permission) {
        /**
         * {
         *     code:209,
         *     message:"没有权限”
         * }
         */
        response.setStatusCode(HttpStatus.OK);
        //给响应体放json串
        HttpHeaders headers = response.getHeaders();
        headers.set(HttpHeaders.CONTENT_TYPE, "application/json;charset=utf-8");


        //普通编程。数据库查1号数据。  Person person = dao.findById(1);
        //数据库数据做成一个 Publisher。非阻塞  Mono  Flux
        //  dao.findById(1) == Publisher;  =======   Consumer(person)->{}
        // ajax 回调机制

        DataBufferFactory dataBufferFactory = response.bufferFactory();
        //构造rsult数据
        Result<Object> build = Result.build(null, permission);

        String s = null;
        try {
            //将result转换json
            s = new ObjectMapper().writeValueAsString(build);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }

        //Mono\Flux 返回流式数据
        return response.writeWith(Mono.just(dataBufferFactory.wrap(s.getBytes())));
    }
}
